package com.gomcarter.frameworks.base.config;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.gomcarter.frameworks.base.common.JsonMapper;
import com.gomcarter.frameworks.base.common.RequestUtils;
import com.gomcarter.frameworks.base.converter.DateEditor;
import com.gomcarter.frameworks.base.converter.LocalDateEditor;
import com.gomcarter.frameworks.base.converter.LocalDateTimeEditor;
import com.gomcarter.frameworks.base.converter.LocalTimeEditor;
import com.gomcarter.frameworks.base.exception.CustomException;
import com.gomcarter.frameworks.base.json.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.annotation.Resource;
import javax.management.ReflectionException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 响应配置
 *
 * @author z.y.h
 * create on 2023/3/3 13:14
 */
@Slf4j
@ControllerAdvice
public class BaseRespAdvice implements ResponseBodyAdvice<Object> {
    private static final String RESULT_WRAPPER_FLAG_KEY = "X-Result-Wrapper";
    @Resource
    private BaseConfProperties properties;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
        return !methodParameter.hasMethodAnnotation(JsonIgnore.class);
    }

    @Override
    public Object beforeBodyWrite(Object obj, MethodParameter methodParameter, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (properties.isResultWrapperHeadEnabled()) {
            HttpHeaders headers = serverHttpRequest.getHeaders();
            List<String> headerValue = headers.get(RESULT_WRAPPER_FLAG_KEY);
            if (!ObjectUtils.isEmpty(headerValue) && !Boolean.parseBoolean(headerValue.get(0))) {
                return obj;
            }
        }
        if (obj == null) {
            return new JsonSuccess();
        } else if (obj instanceof JsonObject) {
            return obj;
        } else if (obj instanceof ModelAndView) {
            return obj;
        } else {
            return new JsonData(obj);
        }
    }

    @ResponseBody
    @ExceptionHandler(Throwable.class)
    public ResponseEntity<?> exceptionHandler(HttpServletRequest request, Exception exception, HandlerMethod handlerMethod) {
        log.error("{}操作失败, url:{}, method:{}, ip: {}, Referer: {}, UA: {}, params: {}, cookie: {},header: {},",
                this.getClass().getName(),
                request.getRequestURI(),
                request.getMethod(),
                RequestUtils.getIp(request),
                request.getHeader("Referer"),
                request.getHeader("User-Agent"),
                JsonMapper.buildNonNullMapper().toJson(request.getParameterMap()),
                JsonMapper.buildNonNullMapper().toJson(request.getCookies()),
                JsonMapper.buildNonNullMapper().toJson(RequestUtils.headerMap(request)),
                exception);
        // 使用jsonIgnore注解的时候自定义异常返回http状态码为500,并且返回真实的异常原因(目前这是一个折中方案, 针对自定义异常是否返回200还有待确定)
        boolean jsonIgnore = handlerMethod != null && handlerMethod.hasMethodAnnotation(JsonIgnore.class);
        // HttpStatus:200: 自定义异常
        if (exception instanceof CustomException) {
            HttpStatus httpStatus = jsonIgnore ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.OK;
            return doResponse(httpStatus, new JsonError(exception.getMessage(), ((CustomException) exception).getCode()));
        }
        // HttpStatus:400
        // 参数异常
        if (exception instanceof MissingServletRequestParameterException) {
            return doResponse(HttpStatus.BAD_REQUEST, new JsonError(exception.getMessage(), ErrorCode.paramError.getCode()));
        }
        // 接口参数不匹配异常
        if (exception instanceof MethodArgumentNotValidException) {
            return doResponse(HttpStatus.BAD_REQUEST, new JsonError(getErrorInfo(((MethodArgumentNotValidException) exception)
                    .getBindingResult()), ErrorCode.paramError.getCode()));
        }
        // 参数绑定异常
        if (exception instanceof BindException) {
            return doResponse(HttpStatus.BAD_REQUEST, new JsonError(getErrorInfo(((BindException) exception).getBindingResult()),
                    ErrorCode.paramError.getCode()));
        }
        // 参数约束条件异常
        if (exception instanceof ConstraintViolationException) {
            return doResponse(HttpStatus.BAD_REQUEST, new JsonError(((ConstraintViolationException) exception)
                    .getConstraintViolations()
                    .stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(";")), ErrorCode.paramError.getCode()));
        }
        // 非法参数异常
        if (exception instanceof IllegalArgumentException) {
            return doResponse(HttpStatus.BAD_REQUEST, new JsonError(exception.getMessage(), ErrorCode.illArgError.getCode()));
        }

        // HttpStatus:500
        //空指针异常
        if (exception instanceof NullPointerException) {
            return doResponse(HttpStatus.INTERNAL_SERVER_ERROR, buildJsonError(jsonIgnore, exception, ErrorCode.nullPointer));
        }
        // SQL异常 && 反射异常
        if (exception instanceof SQLException || exception instanceof ReflectionException) {
            return doResponse(HttpStatus.INTERNAL_SERVER_ERROR, buildJsonError(jsonIgnore, exception, ErrorCode.sqlError));
        }
        // 其他异常
        return doResponse(HttpStatus.INTERNAL_SERVER_ERROR, buildJsonError(jsonIgnore, exception, ErrorCode.defaultError));
    }

    private JsonError buildJsonError(boolean useRealErrorMsg, Exception exception, ErrorCode defaultErrorCode) {
        String errorMsg = useRealErrorMsg ? exception.getMessage() : defaultErrorCode.getMsg();
        if (useRealErrorMsg && exception instanceof NullPointerException) {
            // 空指针异常如果使用exception.getMessage得到的值是null, 会对调用方产生误导, 因此使用exception.toString() 得到java.lang.NullPointerException加以区分
            errorMsg = exception.toString();
        }
        return new JsonError(errorMsg, defaultErrorCode.getCode());
    }

    private <T> ResponseEntity<T> doResponse(HttpStatus status, T body) {
        return ResponseEntity.status(status).body(body);
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setAutoGrowCollectionLimit(properties.getAutoGrowCollectionLimit());
        binder.registerCustomEditor(Date.class, new DateEditor());
        binder.registerCustomEditor(LocalTime.class, new LocalTimeEditor());
        binder.registerCustomEditor(LocalDate.class, new LocalDateEditor());
        binder.registerCustomEditor(LocalDateTime.class, new LocalDateTimeEditor());
    }

    private String getErrorInfo(BindingResult bindingResult) {
        StringBuilder stringBuilder = new StringBuilder();
        bindingResult.getFieldErrors().forEach((error) -> {
            stringBuilder.append("[").append(error.getField()).append("]")
                    .append(error.getDefaultMessage()).append(";");
        });
        return stringBuilder.toString();
    }
}